const ohm = require('ohm-js');
const myGrammar = ohm.grammar(`
JSON {
  Value =
    Object
    | Array
    | String
    | Number
    | True
    | False
    | Null

  Object =
    "{" "}" -- empty
    | "{" Pair ("," Pair)* "}" -- nonEmpty

  Pair =
    String ":" Value

  Array =
    "[" "]" -- empty
    | "[" Value ("," Value)* "]" -- nonEmpty

  String =
    "\\"" doubleStringCharacter* "\\""

  doubleStringCharacter (character) =
    ~("\\"" | "\\\\") any -- nonEscaped
    | "\\\\" escapeSequence -- escaped

  escapeSequence =
    "\\"" -- doubleQuote
    | "\\\\" -- reverseSolidus
    | "/" -- solidus
    | "b" -- backspace
    | "f" -- formfeed
    | "n" -- newline
    | "r" -- carriageReturn
    | "t" -- horizontalTab
    | "u" fourHexDigits -- codePoint

  fourHexDigits = hexDigit hexDigit hexDigit hexDigit

  Number =
    decimal exponent -- withExponent
    | decimal -- withoutExponent

  decimal =
    wholeNumber "." digit+ -- withFract
    | wholeNumber -- withoutFract

  wholeNumber =
    "-" unsignedWholeNumber -- negative
    | unsignedWholeNumber -- nonNegative

  unsignedWholeNumber =
    "0" -- zero
    | nonZeroDigit digit* -- nonZero

  nonZeroDigit = "1".."9"

  exponent =
    exponentMark ("+"|"-") digit+ -- signed
    | exponentMark digit+ -- unsigned

  exponentMark = "e" | "E"

  True = "true"
  False = "false"
  Null = "null"
}
`);

const parse = function parse(str) {
  const m = myGrammar.match(str);
  return semantics(m).eval();
};

const semantics = myGrammar.createSemantics().addOperation('eval', {
  Object_nonEmpty(_0, item, _1, items, _2) {
    const obj = {};
    obj[item.children[0].eval()] = item.children[2].eval();
    for (let d of items.children) {
      obj[d.children[0].eval()] = d.children[2].eval();
    }
    return obj;
  },
  Object_empty (_0, _1) {
    return {};
  },
  Array_nonEmpty (_0, item, _1, items, _2) {
    return [item.eval()].concat(items.children.map(x => x.eval()));
  },
  Array_empty (_0, _1) {
    return [];
  },
  String(_0, item, _1) {
    return item.children.map(x => x.eval()).join("");
  },
  doubleStringCharacter_nonEscaped(item) { return item.sourceString; },
  doubleStringCharacter_escaped(_, item) { return item.eval(); },
  escapeSequence_doubleQuote (_) { return '"'; },
  escapeSequence_reverseSolidus (_) { return '\\'; },
  escapeSequence_solidus (_) { return '/'; },
  escapeSequence_backspace (_) { return '\b'; },
  escapeSequence_formfeed (_) { return '\f'; },
  escapeSequence_newline (_) { return '\n'; },
  escapeSequence_carriageReturn (_) { return '\r'; },
  escapeSequence_horizontalTab (_) { return '\t'; },
  escapeSequence_codePoint (_, item) {
    return String.fromCharCode(parseInt(item.sourceString, 16));
  },
  Number(item) {
    return parseFloat(item.sourceString, 10);
  },
  True(_) {
    return true;
  },
  False(_) {
    return false;
  },
  Null(_) {
    return null;
  },
});

export default {
  parse,
};
